// FastZip.cs
//
// Copyright 2005 John Reilly
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// Linking this library statically or dynamically with other modules is
// making a combined work based on this library.  Thus, the terms and
// conditions of the GNU General Public License cover the whole
// combination.
// 
// As a special exception, the copyright holders of this library give you
// permission to link this library with independent modules to produce an
// executable, regardless of the license terms of these independent
// modules, and to copy and distribute the resulting executable under
// terms of your choice, provided that you also meet, for each linked
// independent module, the terms and conditions of the license of that
// module.  An independent module is a module which is not derived from
// or based on this library.  If you modify this library, you may extend
// this exception to your version of the library, but you are not
// obligated to do so.  If you do not wish to do so, delete this
// exception statement from your version.

//  2010-08-13  Sky Sanders - Modified for Silverlight 3/4 and Windows Phone 7

// TODO: implement IsolatedStorage aware implementations


using System;
using System.IO;
using System.IO.IsolatedStorage;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using Salient.SharpZipLib.Core;

namespace Salient.SharpZipLib.Zip
{
  
	
	/// <summary>
	/// FastZip provides facilities for creating and extracting zip files.
	/// </summary>
	public class IsolatedFastZip
	{
		#region Enumerations
		/// <summary>
		/// Defines the desired handling when overwriting files during extraction.
		/// </summary>
		public enum Overwrite 
		{
			/// <summary>
			/// Prompt the user to confirm overwriting
			/// </summary>
			Prompt,
			/// <summary>
			/// Never overwrite files.
			/// </summary>
			Never,
			/// <summary>
			/// Always overwrite files.
			/// </summary>
			Always
		}
		#endregion
		
		#region Constructors
		/// <summary>
		/// Initialise a default instance of <see cref="FastZip"/>.
		/// </summary>
		public IsolatedFastZip()
		{
		}

		/// <summary>
		/// Initialise a new instance of <see cref="FastZip"/>
		/// </summary>
		/// <param name="events">The <see cref="FastZipEvents">events</see> to use during operations.</param>
        public IsolatedFastZip(FastZipEvents events)
		{
			events_ = events;
		}
		#endregion
		
		#region Properties
		/// <summary>
		/// Get/set a value indicating wether empty directories should be created.
		/// </summary>
		public bool CreateEmptyDirectories
		{
			get { return createEmptyDirectories_; }
			set { createEmptyDirectories_ = value; }
		}


		/// <summary>
		/// Get / set the password value.
		/// </summary>
		public string Password
		{
			get { return password_; }
			set { password_ = value; }
		}


		/// <summary>
		/// Get or set the <see cref="INameTransform"></see> active when creating Zip files.
		/// </summary>
		/// <seealso cref="EntryFactory"></seealso>
		public INameTransform NameTransform
		{
			get { return entryFactory_.NameTransform; }
			set {
				entryFactory_.NameTransform = value;
			}
		}

		/// <summary>
		/// Get or set the <see cref="IEntryFactory"></see> active when creating Zip files.
		/// </summary>
		public IEntryFactory EntryFactory
		{
			get { return entryFactory_; }
			set {
				if ( value == null ) {
					entryFactory_ = new IsolatedZipEntryFactory();
				}
				else {
					entryFactory_ = value;
				}
			}
		}
		
		/// <summary>
		/// Gets or sets the setting for <see cref="UseZip64">Zip64 handling when writing.</see>
		/// </summary>
        /// <remarks>
        /// The default value is dynamic which is not backwards compatible with old
        /// programs and can cause problems with XP's built in compression which cant
        /// read Zip64 archives. However it does avoid the situation were a large file
        /// is added and cannot be completed correctly.
        /// NOTE: Setting the size for entries before they are added is the best solution!
        /// By default the EntryFactory used by FastZip will set fhe file size.
        /// </remarks>
		public UseZip64 UseZip64
		{
			get { return useZip64_; }
			set { useZip64_ = value; }
		}
		
		/// <summary>
		/// Get/set a value indicating wether file dates and times should 
		/// be restored when extracting files from an archive.
		/// </summary>
		/// <remarks>The default value is false.</remarks>
		public bool RestoreDateTimeOnExtract
		{
			get {
				return restoreDateTimeOnExtract_;
			}
			set {
				restoreDateTimeOnExtract_ = value;
			}
		}
		
		/// <summary>
		/// Get/set a value indicating wether file attributes should
		/// be restored during extract operations
		/// </summary>
		public bool RestoreAttributesOnExtract
		{
			get { return restoreAttributesOnExtract_; }
			set { restoreAttributesOnExtract_ = value; }
		}
		#endregion
		
		#region Delegates
		/// <summary>
		/// Delegate called when confirming overwriting of files.
		/// </summary>
		public delegate bool ConfirmOverwriteDelegate(string fileName);
		#endregion
		
		#region CreateZip
		/// <summary>
		/// Create a zip file.
		/// </summary>
		/// <param name="zipFileName">The name of the zip file to create.</param>
		/// <param name="sourceDirectory">The directory to source files from.</param>
		/// <param name="recurse">True to recurse directories, false for no recursion.</param>
		/// <param name="fileFilter">The <see cref="PathFilter">file filter</see> to apply.</param>
		/// <param name="directoryFilter">The <see cref="PathFilter">directory filter</see> to apply.</param>
		public void CreateZip(string zipFileName, string sourceDirectory, 
			bool recurse, string fileFilter, string directoryFilter)
		{
		    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
		    {
                CreateZip(store.CreateFile(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter);
		    }
		}
		
		/// <summary>
		/// Create a zip file/archive.
		/// </summary>
		/// <param name="zipFileName">The name of the zip file to create.</param>
		/// <param name="sourceDirectory">The directory to obtain files and directories from.</param>
		/// <param name="recurse">True to recurse directories, false for no recursion.</param>
		/// <param name="fileFilter">The file filter to apply.</param>
		public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter)
		{
		    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
		    {
		        CreateZip(store.CreateFile(zipFileName), sourceDirectory, recurse, fileFilter, null);
		    }
		}

		/// <summary>
		/// Create a zip archive sending output to the <paramref name="outputStream"/> passed.
		/// </summary>
		/// <param name="outputStream">The stream to write archive data to.</param>
		/// <param name="sourceDirectory">The directory to source files from.</param>
		/// <param name="recurse">True to recurse directories, false for no recursion.</param>
		/// <param name="fileFilter">The <see cref="PathFilter">file filter</see> to apply.</param>
		/// <param name="directoryFilter">The <see cref="PathFilter">directory filter</see> to apply.</param>
        /// <remarks>The <paramref name="outputStream"/> is closed after creation.</remarks>
		public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter)
		{
			NameTransform = new ZipNameTransform(sourceDirectory);
			sourceDirectory_ = sourceDirectory;

			using ( outputStream_ = new ZipOutputStream(outputStream) ) {


				if ( password_ != null ) {
					outputStream_.Password = password_;
				}


				outputStream_.UseZip64 = UseZip64;
				var scanner = new IsolatedFileSystemScanner(fileFilter, directoryFilter);
				scanner.ProcessFile += ProcessFile;
				if ( CreateEmptyDirectories ) {
					scanner.ProcessDirectory += ProcessDirectory;
				}
				
				if (events_ != null) {
					if ( events_.FileFailure != null ) {
						scanner.FileFailure += events_.FileFailure;
					}

					if ( events_.DirectoryFailure != null ) {
						scanner.DirectoryFailure += events_.DirectoryFailure;
					}
				}

				scanner.Scan(sourceDirectory, recurse);
			}
		}

		#endregion
		
		#region ExtractZip
		/// <summary>
		/// Extract the contents of a zip file.
		/// </summary>
		/// <param name="zipFileName">The zip file to extract from.</param>
		/// <param name="targetDirectory">The directory to save extracted information in.</param>
		/// <param name="fileFilter">A filter to apply to files.</param>
		public void ExtractZip(string zipFileName, string targetDirectory, string fileFilter) 
		{
			ExtractZip(zipFileName, targetDirectory, Overwrite.Always, null, fileFilter, null, restoreDateTimeOnExtract_);
		}
		
		/// <summary>
		/// Extract the contents of a zip file.
		/// </summary>
		/// <param name="zipFileName">The zip file to extract from.</param>
		/// <param name="targetDirectory">The directory to save extracted information in.</param>
		/// <param name="overwrite">The style of <see cref="Overwrite">overwriting</see> to apply.</param>
		/// <param name="confirmDelegate">A delegate to invoke when confirming overwriting.</param>
		/// <param name="fileFilter">A filter to apply to files.</param>
		/// <param name="directoryFilter">A filter to apply to directories.</param>
		/// <param name="restoreDateTime">Flag indicating whether to restore the date and time for extracted files.</param>
		public void ExtractZip(string zipFileName, string targetDirectory, Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, string fileFilter, string directoryFilter, bool restoreDateTime)
		{
		    Stream inputStream;
		    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
		    {
		        inputStream = store.OpenFile(zipFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
		    }
		    ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true);
		}

		/// <summary>
		/// Extract the contents of a zip file held in a stream.
		/// </summary>
		/// <param name="inputStream">The seekable input stream containing the zip to extract from.</param>
		/// <param name="targetDirectory">The directory to save extracted information in.</param>
		/// <param name="overwrite">The style of <see cref="Overwrite">overwriting</see> to apply.</param>
		/// <param name="confirmDelegate">A delegate to invoke when confirming overwriting.</param>
		/// <param name="fileFilter">A filter to apply to files.</param>
		/// <param name="directoryFilter">A filter to apply to directories.</param>
		/// <param name="restoreDateTime">Flag indicating whether to restore the date and time for extracted files.</param>
		/// <param name="isStreamOwner">Flag indicating whether the inputStream will be closed by this method.</param>
		public void ExtractZip(Stream inputStream, string targetDirectory,Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate,string fileFilter, string directoryFilter, bool restoreDateTime,bool isStreamOwner)
		{
			if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null)) {
				throw new ArgumentNullException("confirmDelegate");
			}

			continueRunning_ = true;
			overwrite_ = overwrite;
			confirmDelegate_ = confirmDelegate;
			extractNameTransform_ = new IsolatedNameTransform(targetDirectory);

			fileFilter_ = new NameFilter(fileFilter);
			directoryFilter_ = new NameFilter(directoryFilter);
			restoreDateTimeOnExtract_ = restoreDateTime;

			using (zipFile_ = new IsolatedZipFile(inputStream)) {


				if (password_ != null) {
					zipFile_.Password = password_;
				}

				zipFile_.IsStreamOwner = isStreamOwner;
				System.Collections.IEnumerator enumerator = zipFile_.GetEnumerator();
				while (continueRunning_ && enumerator.MoveNext()) {
					ZipEntry entry = (ZipEntry)enumerator.Current;
					if (entry.IsFile)
					{
						// TODO Path.GetDirectory can fail here on invalid characters.
						if (directoryFilter_.IsMatch(Path.GetDirectoryName(entry.Name)) && fileFilter_.IsMatch(entry.Name)) {
							ExtractEntry(entry);
						}
					}
					else if (entry.IsDirectory) {
						if (directoryFilter_.IsMatch(entry.Name) && CreateEmptyDirectories) {
							ExtractEntry(entry);
						}
					}
					else {
						// Do nothing for volume labels etc...
					}
				}
			}
		}
		#endregion
		
		#region Internal Processing
		void ProcessDirectory(object sender, DirectoryEventArgs e)
		{
			if ( !e.HasMatchingFiles && CreateEmptyDirectories ) {
				if ( events_ != null ) {
					events_.OnProcessDirectory(e.Name, e.HasMatchingFiles);
				}
				
				if ( e.ContinueRunning ) {
					if (e.Name != sourceDirectory_) {
						ZipEntry entry = entryFactory_.MakeDirectoryEntry(e.Name);
						outputStream_.PutNextEntry(entry);
					}
				}
			}
		}
		
		void ProcessFile(object sender, ScanEventArgs e)
		{
			if ( (events_ != null) && (events_.ProcessFile != null) ) {
				events_.ProcessFile(sender, e);
			}
			
			if ( e.ContinueRunning ) {
                try {
                    // The open below is equivalent to OpenRead which gaurantees that if opened the 
                    // file will not be changed by subsequent openers, but precludes opening in some cases
                    // were it could succeed.
                    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
                    {
                        using (var stream = store.OpenFile(e.Name, FileMode.Open, FileAccess.Read, FileShare.Read))
                        {
                            ZipEntry entry = entryFactory_.MakeFileEntry(e.Name);
                            outputStream_.PutNextEntry(entry);
                            AddFileContents(e.Name, stream);
                        }
                    }
                }
                catch(Exception ex) {
                    if (events_ != null) {
                        continueRunning_ = events_.OnFileFailure(e.Name, ex);
                    }
                    else {
                        continueRunning_ = false;
                        throw;
                    }
                }
			}
		}

		void AddFileContents(string name, Stream stream)
		{
			if( stream==null ) {
				throw new ArgumentNullException("stream");
			}

			if( buffer_==null ) {
				buffer_=new byte[4096];
			}

			if( (events_!=null)&&(events_.Progress!=null) ) {
				StreamUtils.Copy(stream, outputStream_, buffer_,
					events_.Progress, events_.ProgressInterval, this, name);
			}
			else {
				StreamUtils.Copy(stream, outputStream_, buffer_);
			}

			if( events_!=null ) {
				continueRunning_=events_.OnCompletedFile(name);
			}
		}

		void ExtractFileEntry(ZipEntry entry, string targetName)
		{
			bool proceed = true;
		    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
		    {
		        if ( overwrite_ != Overwrite.Always ) {
		            if (store.FileExists(targetName))
		            {
		                if ( (overwrite_ == Overwrite.Prompt) && (confirmDelegate_ != null) ) {
		                    proceed = confirmDelegate_(targetName);
		                }
		                else {
		                    proceed = false;
		                }
		            }
		        }
			
		        if ( proceed ) {
		            if ( events_ != null ) {
		                continueRunning_ = events_.OnProcessFile(entry.Name);
		            }
			
		            if ( continueRunning_ ) {
		                try {
                            using (var outputStream = store.CreateFile(targetName))
		                    {
		                        if ( buffer_ == null ) {
		                            buffer_ = new byte[4096];
		                        }
		                        if ((events_ != null) && (events_.Progress != null))
		                        {
		                            StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_,
		                                             events_.Progress, events_.ProgressInterval, this, entry.Name, entry.Size);
		                        }
		                        else
		                        {
		                            StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_);
		                        }
							
		                        if (events_ != null) {
		                            continueRunning_ = events_.OnCompletedFile(entry.Name);
		                        }
		                    }

		                }
		                catch(Exception ex) {
		                    if ( events_ != null ) {
		                        continueRunning_ = events_.OnFileFailure(targetName, ex);
		                    }
		                    else {
		                        continueRunning_ = false;
		                        throw;
		                    }
		                }
		            }
		        }
		    }
		}

		void ExtractEntry(ZipEntry entry)
		{
			bool doExtraction = entry.IsCompressionMethodSupported();
			string targetName = entry.Name;
		    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
		    {
		        if ( doExtraction ) {
		            if ( entry.IsFile ) {
		                targetName = extractNameTransform_.TransformFile(targetName);
		            }
		            else if ( entry.IsDirectory ) {
		                targetName = extractNameTransform_.TransformDirectory(targetName);
		            }
				
		            doExtraction = !string.IsNullOrEmpty(targetName);
		        }
			
		        // TODO: Fire delegate/throw exception were compression method not supported, or name is invalid?

		        string dirName = null;
			
		        if ( doExtraction ) {
		            if ( entry.IsDirectory ) {
		                dirName = targetName;
		            }
		            else {
		                dirName = Path.GetDirectoryName(targetName);
		            }
		        }

		        if (doExtraction && !store.DirectoryExists(dirName))
		        {
		            if ( !entry.IsDirectory || CreateEmptyDirectories ) {
		                try {
		                    store.CreateDirectory(dirName);
		                }
		                catch (Exception ex) {
		                    doExtraction = false;
		                    if ( events_ != null ) {
		                        if ( entry.IsDirectory ) {
		                            continueRunning_ = events_.OnDirectoryFailure(targetName, ex);
		                        }
		                        else {
		                            continueRunning_ = events_.OnFileFailure(targetName, ex);
		                        }
		                    }
		                    else {
		                        continueRunning_ = false;
		                        throw;
		                    }
		                }
		            }
		        }
		    }

		    if ( doExtraction && entry.IsFile ) {
				ExtractFileEntry(entry, targetName);
			}
		}

		static int MakeExternalAttributes(FileInfo info)
		{
			return (int)info.Attributes;
		}

        static bool NameIsValid(string name)
        {
            return (!string.IsNullOrEmpty(name)) && (name.IndexOfAny(Path.GetInvalidPathChars()) < 0);
        }
		#endregion
		
		#region Instance Fields
		bool continueRunning_;
		byte[] buffer_;
		ZipOutputStream outputStream_;
		IsolatedZipFile zipFile_;
		string sourceDirectory_;
		NameFilter fileFilter_;
		NameFilter directoryFilter_;
		Overwrite overwrite_;
		ConfirmOverwriteDelegate confirmDelegate_;
		
		bool restoreDateTimeOnExtract_;
		bool restoreAttributesOnExtract_;
		bool createEmptyDirectories_;
		FastZipEvents events_;
		IEntryFactory entryFactory_ = new IsolatedZipEntryFactory();
		INameTransform extractNameTransform_;
		UseZip64 useZip64_=UseZip64.Dynamic;
		string password_;

		#endregion
	}
}
